/*******************************************************************************
 * Copyright (c) 2008, 2009 Tapestry IDE Developers
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Tapestry IDE Developers - initial API and implementation
 *******************************************************************************/
package com.googlecode.tapestry.ide.internal.core.model;

import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.ui.PlatformUI;

import com.googlecode.tapestry.ide.TapestryPlugin;
import com.googlecode.tapestry.ide.core.TapestryCoreUtils;
import com.googlecode.tapestry.ide.core.model.AbstractModel;
import com.googlecode.tapestry.ide.core.model.IModelElement;
import com.googlecode.tapestry.ide.core.model.ITapestryItem;
import com.googlecode.tapestry.ide.core.model.ITapestryModel;
import com.googlecode.tapestry.ide.core.model.ITapestryProject;
import com.googlecode.tapestry.ide.core.model.ModelChangeEvent.Type;
import com.googlecode.tapestry.ide.core.model.resources.TapestryResourceChangeListener;
import com.googlecode.tapestry.ide.internal.core.model.resources.ITapestryResourceChangeEvents;

/**
 * This model manages instances of {@link IProject}s. It's populated from
 * Eclipse's current workspace and receives {@link IResourceChangeEvent}s for
 * workspaces changes.
 * <p>
 * The single instance of {@link ITapestryModel} is available from the static
 * method {@link TapestryPlugin#getModel()}.
 */
public class TapestryModel extends AbstractModel implements ITapestryModel {

	/** The rwl. */
	private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

	/** The r. */
	private final Lock r = rwl.readLock();

	/** The w. */
	private final Lock w = rwl.writeLock();

	/** The table of Tapestry projects (synchronized for concurrent access). */
	protected Map<IProject, ITapestryProject> projects;

	/** The workspace listener. */
	private IResourceChangeListener workspaceListener;

	/**
	 * Instantiates a new tapestry model.
	 */
	public TapestryModel() {
		super(null, ITapestryModel.ELEMENT_NAME);
		projects = new ConcurrentHashMap<IProject, ITapestryProject>();
	}

	/**
	 * Called on startup of the plug-in. Should return as fast as possible
	 * because it blocks the application.
	 */
	public void startup() {
		try {
			w.lock();

			projects.clear();
			for (IProject project : TapestryCoreUtils.getTapestryProjects()) {
				TapestryProject proj = new TapestryProject(this, project);
				projects.put(project, proj);
			}
		} finally {
			w.unlock();
		}

		// TODO: when combination of workspace listener and lazy loading

		workspaceListener = new TapestryResourceChangeListener(
				new ResourceChangeEventHandler());
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		workspace.addResourceChangeListener(workspaceListener,
				TapestryResourceChangeListener.LISTENER_FLAGS);
	}

	/**
	 * Shutdown.
	 */
	public void shutdown() {

		// Remove the ResourceChangeListener from the Eclipse Workspace
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		workspace.removeResourceChangeListener(workspaceListener);
		workspaceListener = null;

		try {
			w.lock();
			projects.clear();
		} finally {
			w.unlock();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @seecom.googlecode.tapestry.ide.core.model.AbstractModelElement#
	 * getElementChildren ()
	 */
	@Override
	public IModelElement[] getElementChildren() {
		IModelElement[] elements = new IModelElement[projects.size()];
		int i = 0;
		for (ITapestryProject tapestryProject : getProjects()) {
			elements[i++] = tapestryProject;
		}
		return elements;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.googlecode.tapestry.ide.core.model.ITapestryModel#getProject(org.
	 * eclipse .core.resources.IProject)
	 */
	@Override
	public ITapestryProject getProject(final IProject project) {
		try {
			r.lock();
			final TapestryProject tapestryProject = (TapestryProject) projects
					.get(project);
			if (tapestryProject != null && (!tapestryProject.isInitialized())) {
				try {
					PlatformUI.getWorkbench().getProgressService().run(false,
							false, new IRunnableWithProgress() {

								@Override
								public void run(IProgressMonitor monitor)
										throws InvocationTargetException,
										InterruptedException {
									monitor
											.setTaskName("Loading Tapestry model for project "
													+ project.getName());
									tapestryProject.init();
								}
							});
				} catch (InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			return tapestryProject;
		} finally {
			r.unlock();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.googlecode.tapestry.ide.core.model.ITapestryModel#getProjects()
	 */
	@Override
	public Set<ITapestryProject> getProjects() {
		try {
			r.lock();
			final Set<ITapestryProject> tapestryProjects = new HashSet<ITapestryProject>();
			boolean shouldInit = false;
			for (IProject project : projects.keySet()) {
				TapestryProject tapestryProject = (TapestryProject) projects
						.get(project);
				tapestryProjects.add(tapestryProject);
				if (!tapestryProject.isInitialized()) {
					shouldInit = true;
				}
			}
			if (shouldInit) {
				try {
					PlatformUI.getWorkbench().getProgressService().run(false,
							false, new IRunnableWithProgress() {

								@Override
								public void run(IProgressMonitor monitor)
										throws InvocationTargetException,
										InterruptedException {
									monitor
											.setTaskName("Loading Tapestry model for projects");
									for (ITapestryProject project : tapestryProjects) {
										TapestryProject tapestryProject = (TapestryProject) project;
										if (!tapestryProject.isPopulated()) {
											tapestryProject.init();
										}
									}
								}
							});
				} catch (InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			return tapestryProjects;
		} finally {
			r.unlock();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.googlecode.tapestry.ide.core.model.ITapestryModel#hasProject(org.
	 * eclipse .core.resources.IProject)
	 */
	@Override
	public boolean hasProject(IProject project) {
		try {
			r.lock();
			return projects.containsKey(project);
		} finally {
			r.unlock();
		}
	}

	/**
	 * Internal resource change event handler.
	 */
	private class ResourceChangeEventHandler implements
			ITapestryResourceChangeEvents {

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #isTapestryProject(org.eclipse.core.resources.IProject, int)
		 */
		public boolean isTapestryProject(IProject project, int eventType) {
			try {
				r.lock();
				return projects.get(project) != null;
			} finally {
				r.unlock();
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #tapestryNatureAdded(org.eclipse.core.resources.IProject, int)
		 */
		public void tapestryNatureAdded(IProject project, int eventType) {
			if (eventType == IResourceChangeEvent.POST_BUILD) {
				ITapestryProject proj = new TapestryProject(TapestryModel.this,
						project);
				try {
					w.lock();
					projects.put(project, proj);
				} finally {
					w.unlock();
				}

				// Need ADD here because for the Tapestry Explorer the according
				// ITapestryProject node has to appear in the CommonNavigator
				notifyListeners(proj, Type.ADDED);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #tapestryNatureRemoved(org.eclipse.core.resources.IProject, int)
		 */
		public void tapestryNatureRemoved(IProject project, int eventType) {
			if (eventType == IResourceChangeEvent.POST_BUILD) {
				ITapestryProject proj = null;
				try {
					w.lock();
					proj = projects.remove(project);
				} finally {
					w.unlock();
				}

				// Need REMOVE here because for the Tapestry Explorer the
				// according
				// ITapestryProject node has to disappear in the
				// CommonNavigator
				if (proj != null) {
					notifyListeners(proj, Type.REMOVED);
				}
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #projectAdded(org.eclipse.core.resources.IProject, int)
		 */
		public void projectAdded(IProject project, int eventType) {
			if (eventType == IResourceChangeEvent.POST_BUILD) {
				ITapestryProject proj = new TapestryProject(TapestryModel.this,
						project);
				try {
					w.lock();
					projects.put(project, proj);
				} finally {
					w.unlock();
				}
				notifyListeners(proj, Type.ADDED);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #projectOpened(org.eclipse.core.resources.IProject, int)
		 */
		public void projectOpened(IProject project, int eventType) {
			if (eventType == IResourceChangeEvent.POST_BUILD) {
				ITapestryProject proj = new TapestryProject(TapestryModel.this,
						project);
				try {
					w.lock();
					projects.put(project, proj);
				} finally {
					w.unlock();
				}
				notifyListeners(proj, Type.ADDED);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #projectClosed(org.eclipse.core.resources.IProject, int)
		 */
		public void projectClosed(IProject project, int eventType) {
			ITapestryProject proj = null;
			try {
				w.lock();
				proj = projects.remove(project);
			} finally {
				w.unlock();
			}
			if (proj != null) {
				notifyListeners(proj, Type.REMOVED);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #projectDeleted(org.eclipse.core.resources.IProject, int)
		 */
		public void projectDeleted(IProject project, int eventType) {
			ITapestryProject proj = null;
			try {
				w.lock();
				proj = projects.remove(project);
			} finally {
				w.unlock();
			}
			if (proj != null) {
				notifyListeners(proj, Type.REMOVED);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #itemAdded(org.eclipse.core.resources.IFile, int)
		 */
		@Override
		public void resourceAdded(IFile file, int eventType) {
			if (eventType == IResourceChangeEvent.POST_BUILD) {
				TapestryProject project = null;
				try {
					r.lock();
					project = (TapestryProject) projects.get(file.getProject());
				} finally {
					r.unlock();
				}
				if (project.isPopulated()) {
					try {
						ITapestryItem item = project.fileAdded(file);
						if (item != null) {
							notifyListeners(item, Type.ADDED);
						}
					} catch (JavaModelException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #resourceChanged(org.eclipse.core.resources.IFile, int)
		 */
		@Override
		public void resourceChanged(IFile file, int eventType) {
			// TODO Auto-generated method stub

		}

		/*
		 * (non-Javadoc)
		 * 
		 * @seecom.googlecode.tapestry.ide.internal.core.model.resources.
		 * ITapestryResourceChangeEvents
		 * #resourceRemoved(org.eclipse.core.resources.IFile, int)
		 */
		@Override
		public void resourceRemoved(IFile file, int eventType) {
			if (eventType == IResourceChangeEvent.POST_BUILD) {
				TapestryProject project = null;
				try {
					r.lock();
					project = (TapestryProject) projects.get(file.getProject());
					// file.
				} finally {
					r.unlock();
				}
				if (project.isPopulated()) {
					try {
						ITapestryItem item = project.fileRemoved(file);
						if (item != null) {
							if (item.getCompilationUnit().getResource().equals(
									file)) {
								notifyListeners(item.getElementParent(),
										Type.REMOVED);
							} else {
								notifyListeners(item, Type.CHANGED);
							}
						}
					} catch (JavaModelException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}
	}

}
